std::functiona deep dive behind the curtain Meeting C++ 2022
Andreas Reischuck @arBmind
My Name is Andreas Reischuck … also known as arBmind on the interwebs.
I give online and on-site trainings:
You can also hire my company…
Help with C++, Qt and moreWork with us
Not an Expert Disclaimer
Like everybody on this planet, I am just learning!
Take everything I say with the grain of salt.
The code works, but might not be production ready.
In fact I might have simplified it intendionally to bring my points accross.
With all this taken care of, let us investigate std::function.
Let’s go
First, I would like to bring back your memories about normal C++ functions…
Simple C++ function void basic_function() {}
auto trailing_basic_function() -> void {}
0 - This is a very simple function in C++.
Since C++11 we can also write it like this. Many C++ developers don’t know this. It’s called the trailing return type. It was added for consistency with lambda return types.
Both variants are implementations of an actual function.
Function signature are types using VoidFunction = void();
using TrailingVoidFunction = auto() -> void;
In C++ function signatures are types.
For a simple void function Basically: Leave out the name and body ⇒ function signature The function signature is a type. This also works with trailing return types…
These signature types are cannot be stored in a variable.
The compiler simply does not know the size.
But we can …
Variable with pointer to function using VoidFunction = void();
using VoidFunctionPtr = VoidFunction*;
auto basic_function_ptr =
VoidFunctionPtr{&basic_function};
basic_function_ptr(); // calls basic_function
0 - create a pointer to a function with this…
Pointers can be stored in variables. address of a function is a function pointer. We can call the function pointer directly…
I hope this brings everyone on the same page here.
Any questions so far?
constexpr function pointerconstexpr auto constexpr_basic_function_ptr =
VoidFunctionPtr{&basic_function};
constexpr_basic_function_ptr();
The compiler pretends to know the function pointers at compile time.
We can store them as constexpr variables… …and we can still call them in regular code.
The function does not have to be constexpr. But the pointer to the function always is.
Actual pointer might only be known when the kernel loaded our binary.
This concludes the C++ function pointer introduction. To summarize…
Summary function signatures are types we can store pointers to functions function pointers are known at compile time
1-2-3
Questions?
Okay then…
Why use std::function ? Why do we actually need std::function then?
Let’s consider the following example…
Callback Example
Button triggers callback when clicked.
How should we implement a callback?
For a simple example when a button is clicked.
Pause!
Let’s try to use what we have learned so far…
struct Button {
VoidFunctionPtr clicked;
};
struct EditDialog {
Button okButton;
Button cancelButton;
void onOkClicked();
void onCancelClicked();
};
0 - Our Button should be a C++ class. I use struct to make the slide code shorter. This is not my recommendation.
Store function pointer as member attribute. Let’s use it! With a dialog with… … an ok and cancel button. react when a buttons is clicked.
But this is a problem. We can only store a function pointer in the button. How do we know which dialog instance is meant?
So the actual challenge now is…
Challenge: How can we call the instance methods of Dialog on clicked?
using VoidVoidPtrFunction = void(void*);
struct Button {
void* clicked_instance;
VoidVoidPtrFunction clicked;
};
0 - We don’t know the type of thing that the button is used in.
We extend the function with a void pointer argument.
We extend the Button and store the pointer next to the function pointer.
So we store two pointers per callback now.
As we cannot know the concrete types. It’s not type safe.
Function Pointers Extra pointer C-Style Solution
To summarize. It works but…
We need an extra class pointer This is the C style solution.
Don’t use this if you can avoid it.
But this is the benchmark we have be measured against.
Can we do better with C++?
struct ClickableInterface {
virtual ~ClickableInterface() = default;
virtual void onClicked() = 0;
};
struct EditDialog {
Button okButton;
Button cancelButton;
// Puh…
};
0 - Let’s create a small interface for the Clickable callback.
As it’s very easy to shoot yourself in the foot with C++.
We should never forget the virtual destructor.
Okay. Does this solve the issue? Let’s consider again our dialog. It has two buttons. How do we implement this interface for each of the buttons?
There are ways to do this, but they involve a lot of effort.
Let’s see where we are…
Object Oriented Approach Clickable Interface
This was not a fully working solution, yet.
Before we get stuck here, try something else…
Use std::function
#include <functional>
using CallbackFunc = std::function<void()>;
struct Button {
CallbackFunc clicked;
};
EditDialog::EditDialog() {
okButton.clicked = [this]() {
this->onOkClicked();
};
}
Use std::function
std function is type safe very straight forward to use great stuff!
This callback example is also the 90% use case.
Questions?
But there are some more demanding examples as well…
Extreme Example
Task Scheduler
So consider when a button is clicked you want to compute something.
For example decompress a video with many frames and auto.
So we have many small computing tasks that should be scheduled on a CPU with finite amount of cores.
That’s what our scheduler is supposed to do.
struct Scheduler {
using Task = std::function<void()>;
void queueUpTask(const Task&);
private:
std::queue<Task> queue;
};
0 - We implement the scheduler as a C++ class.
It needs an Api to queue up a new task. Each task can be a std function.
The task shoud be executed on a thread. Store all tasks in a queue.
I hope you get the idea.
All the arguments for each task are captured in the std function. Which might be much more than just a this pointer.
Let’s summarize…
Summary std::function
store any callable type safe small callables
As we have seen in the examples…
We can store any callable object. A lambda for the button handler or a big task like object. Unlike the C Solution it’s type safe But most of our callables are small. Like calling a member function.
std::function is a very useful thing! Good to have.
Questions?
Okay now we know it’s useful. Let’s look how this magic trick works…
Naive implemenation of std::function We could jump directly to the source of the std libraries.
But I suggest to keep these as our final bosses.
Let us try to implement it on our own.
Function template signature using IntFunction = Function<void(int)>;
template</*What goes here?*/>
struct Function {};
template<class Signature>
struct Function;
// Partial Template Specialisation
template<class Ret, class... Args>
struct Function<Ret(Args...)> {};
Start with the interface. We need a template
… Pause … Function signatures are just types. How to access return type and arguments? …Partial template specialisation… function template requires a type Implementation for function signature
We get return type and all the argument types.
Great start! What’s next?
We want our function to be callable…
Call Interface template<class Ret, class... Args>
struct Function<Ret(Args...)> {
/*?*/ operator() (/*args? */) const {
}
Ret operator() (Args... args) const {
return m_ptr->call((Args)args...);
}
struct CallInterface {
virtual Ret call(Args...) = 0;
};
std::shared_ptr<CallInterface> m_ptr;
};
Call Implementation template<class Ret, class... Args>
struct Function<Ret(Args...)> { //
template<class Callable>
struct CallImpl;
template<class Callable>
struct CallImpl final : CallInterface {
Callable m_callable;
Ret call(Args... args) override {
return m_callable((Args)args...);
return std::invoke(m_callable, (Args)args...);
}
};
};
The trick is that the interface implementation is specific to a concrete callable.
Template with the actual callable type implement our call interface. Store an instance of the callable. and implement the interface… By invoking the callable with arguments.
5 - Use std::invoke to call any invokable.
The only missing function part is now the constructor…
Constructor template<class Ret, class... Args>
struct Function<Ret(Args...)> {
template<class Callable>
requires(!std::is_same_v<Callable, Function>)
Function(const Callable& callable)
: m_ptr{new CallImpl<Callable>{callable}} {}
/*snip*/
CallImpl(const Callable& callable)
: m_callable{callable} {}
};
Constructor should allow any callable type.
So we template the constructor… Not accidentally create a new copy constructor. So callable schould not be Function. Construct an instance of the CallImpl store it in our shared pointer. CallImpl needs a constructor as well.
And now? All works!
Good job!
Here is all the code again…
Naive Function Code #include <memory>
template<class Signature> struct Function;
template<class Ret, class... Args>
struct Function<Ret(Args...)> {
Function() = default;
template<class Callable>
requires(!std::is_same_v<Callable, Function>)
Function(const Callable& callable) : m_ptr{new CallImpl<Callable>{callable}} {}
Ret operator() (Args... args) const { return m_ptr->call((Args)args...); }
private:
struct CallInterface {
virtual Ret call(Args...) = 0;
};
template<class Callable>
struct CallImpl final : CallInterface {
Callable m_callable;
CallImpl(const Callable& callable) : m_callable{callable} {}
Ret call(Args... args) override { return std::invoke(m_callable, (Args)args...); }
};
std::shared_ptr<CallInterface> m_ptr;
};
Summary partial template specialisation call interface shared pointer to call interface templated implementation
We used partial template specialisation to extract the arguments from the function signature type Small call interface for the extracted signature We stored a shared pointer to the interface. The implementation of the call interface a template for the specific callable
So it works for our use cases.
Questions to our implementation?
We have some already… ✔ "template signature"✔ "store, copy, and invoke any CopyConstructible Callable"✔ call "operator() "
Empty State If a std::function contains no target, it is called empty.
Invoking the target of an empty std::function results in std::bad_function_call exception being thrown.
cppreference.com
If nothing is stored our function object is empty.
But throwing an exception in this case might be a really bad idea.
This makes the standard implementation unusable in many real time scenarios.
If you ask me: it should do nothing and default construct the result and if that’s not possible it’s undefined behaviour.
This empty state brings some other APIs…
Operator bool explicit operator bool() const noexcept {
return m_ptr;
}
To be able to check for the empty state we have an explicit operator bool.
shared ptr does the same. So the implemenation is easy for us.
Nullptr Constructor Function(std::nullptr_t) noexcept {}
Function& operator=(std::nullptr_t) noexcept {
m_ptr.reset();
return *this;
}
0 - We can explicitly construct a function from nullptr to get the empty state.
1 - We can also assign nullptr to the function object.
Since nullptr is considered the empty state, we can also compare against them…
Nullptr Comparison // note: C++20 generates other variants!
template<class Sig>
bool operator==(const Function<Sig>& f,
std::nullptr_t) noexcept {
return !f;
}
1 - Since C++20 all other overloads are generated by the compiler.
As a side note, we cannot compare two function objects against each other.
If you need that you are out of luck with std::function.
nullptr as an empty indicator might be discussed.
But we can live with that.
So far so reasonable.
Swap is implemented a bit strange…
Swap void swap(Function& other) noexcept {
std::swap(m_ptr, other.m_ptr);
}
template<class Sig>
void swap(Function<Sig> &lhs,
Function<Sig> &rhs) noexcept {
lhs.swap(rhs);
}
0 - First we have a member function swap.
1 - We can easily implement that with swapping our only shared pointer.
2 - Then we have the std::swap overload, that calls the member function.
Next we have some member types we shoud expose…
Member Types using result_type = Ret;
// deprecated in C++17, removed in C++20:
// using argument_type,
// using first_argument_type
// using second_argument_type;
0 - Basically only the result type is required now.
1 - All the others have been deprecated and removed.
In C++17 also all API functions for allocator support were removed.
All these removals seem strange.
Maybe the API was designed in a rush.
But we have some niceties left…
Member pointers … as well as pointers to member functions and pointers to data members.
cppreference.com
Standard functions can not only handle functions but also member functions and data members as well.
What does this actually mean?
Let’s try to use this feature…
Data member pointer usage #include <functional>
struct Example {
int memberData = 2;
};
using F = std::function<int(Example*)>;
int main() {
auto example = Example{};
auto dataFunc = F{&Example::memberData};
std::cout << dataFunc(&example) << '\n';
}
Member function pointer usage #include <functional>
struct Example {
int memberFunction() { return 3; }
};
using F = std::function<int(Example*)>;
int main() {
auto example = Example{};
auto memberFunc = F{&Example::memberFunction};
std::cout << memberFunc(&example) << '\n';
}
0 - member function instead of data member
Same Function to member function Invoke like any function object
By using std::invoke we get this feature for free.
But is it actually useful?
I don’t know. Did anyone had a use case for that?
… Pause …
We have one strange feature left…
Target type const std::type_info& target_type() const noexcept;
template<class T> T* target() noexcept;
template<class T> const T* target() const noexcept;
0 - Query the stored callable type in a std function
note: Requires run time type information (RTTI)
target member function given the correct type T returns a pointer to the stored callable.
>99% of instances make no use of this feature
We pay for the RTTI part anyways.
So let’s summarize the std interface…
Summary std::function interface 🤦♀️ empty state✔ ? nullptr as empty placeholder🤔 swap ✔ ? member pointers❌ target type
We had the basic stuff covered.
Empty state with exceptions is questionable nullptr was easy to implement Swap was a bit strange, but easy to implement Member pointers come for free with std::invoke Target types seem barely useful. I did not bother to show implementation.
Questions on this? … Pause!
If we satisfy all of these are we done yet? Well yes , but there is a recommendations.
Recommended practice: Implementations should avoid the use of dynamically allocated memory for small callable objects, for example, where f’s target is an object holding only a pointer or reference to an object and a member function pointer.
C++ Standard
To paraphrasy the standard.
reduce dynamic memory allocation Not worse than the C solution
Using std::shared_ptr does not satisfy this
So it’s time to take a look at all the other attempts…
Other Implementations "MS-STL" by Microsoft "libstdc++" by GCC project "libc++" by LLVM project "fb-folly" by Facebook "function2" by Naios
Runtime call the function construct destruct, copy, move
Runtime charts have been canceled here!
There are too many variables
How long does a call take? depends haevily on cache state flush and repeat are unrealistic Time for construction will depend on size of your functor Same for destruction, Copy or Move
Measure your use case!
Small Object Optimization
Contestant
x86_32
x86_64
MS-STL
8+2 ptr
6+2 ptr
libstdc++
libc++
fb-folly
function2
MS-STL: 6 pointers plus 16 bytes On 32bits we have 40 bytes total or 10 pointers. 9 for functor + 1 overhead. On 64bits we have 64 bytes total or 8 pointers.
Small Object Optimization
Contestant
x86_32
x86_64
MS-STL
8+2 ptr
6+2 ptr
libstdc++
2+2 ptr
2+2 ptr
libc++
fb-folly
function2
lib-std-c++: 2 pointers for functor
Small Object Optimization
Contestant
x86_32
x86_64
MS-STL
8+2 ptr
6+2 ptr
libstdc++
2+2 ptr
2+2 ptr
libc++
2+? ptr
2+4 ptr
fb-folly
function2
lib-c++: does not compile on 32 bits 2 pointers and 4 pointers overhead
Small Object Optimization
Contestant
x86_32
x86_64
MS-STL
8+2 ptr
6+2 ptr
libstdc++
2+2 ptr
2+2 ptr
libc++
2+? ptr
2+4 ptr
fb-folly
6+2 ptr
6+2 ptr
function2
fb-folly: 8 pointers including 2 as overhead
Small Object Optimization
Contestant
x86_32
x86_64
MS-STL
8+2 ptr
6+2 ptr
libstdc++
2+2 ptr
2+2 ptr
libc++
2+? ptr
2+4 ptr
fb-folly
6+2 ptr
6+2 ptr
function2
6+2 ptr
2+2 ptr
function2: tries to keep size to 32 bytes
Remember: 90% use case uses 2 pointers in C
This looks really bad for C++ here.
Possible Extensions
Customize call qualifiers using F2 = fu2::function<void(int) noexcept>;
// template instance pseudo code:
void function::operator() (int) const noexcept;
template<class Callable>
requires(noexcept(Callable))
function(Callable&);
0 - Example: Require that function is noexcept
What should this mean?
Some Pseudo code The call operator becomes noexcept The constructor… …requires the callable noexcept
Extend for const/non-const functors?
Multiple Overloads using F2 = fu::function<void(int), void(float)>;
struct Example {
void operator() (int x) {
std::print("int: {}\n", x); }
void operator() (double x) {
std::print("double: {}\n", x); }
};
auto f2 = F2{Example{}};
f2(2);
f2(3.14);
More Ideas move only (fu2 & C++23) non-owning (fu2) custom small buffer size (fu2) PMR allocators (fu2) customize empty handling (fu2) constexpr usage disable allocations C-API adapter
Some more ideas…
Do not require functor to be copyable Behave like a pointer or view Tune the small buffer size Allocate using a custom strategy Avoid exceptions on empty state Allow callbacks during compile time Fail to compile if buffer is exceeded Convert to function pointer and context for C-APIs
std::move_only_functionno copy support ✔ forwards call qualifiers correctly✔ can store and invoke any constructible Callable✔ support for inplace construction✔ no more RTTI target_type() or target()
Let’s take a look at C++23 move-only-function
No Copy for functor required Forwards call qualifiers as shown Keeps support for any callable Allows to inplace construct the callable Functions for RTTI were removed
libstdc++ uses 40 bytes. 3 pointers for functor, 2 pointers overhead.
Summary function signatures are types std::function is usefulthe world is bigger than std c++ std::move_only_function in C++23
Andreas Reischuck @arBmind
My Name is Andreas Reischuck … also known as arBmind on the interwebs.
I give online and on-site trainings:
You can also hire my company…
Help with C++, Qt and moreWork with us
… HicknHack Software!
We help you to build better software! We are always hiring! locally in Dresden or 100% remote in Germany
Speaking of Dresden.
Give a Talk
⇒ get a Dresden tour
Rebuild language project
Collaborate
Another topic of my heart!
improved language & tools for everybody Compiler built with C++20
If you are interested in language work I would like to get in contact with you!
Hack more & learn everything!
std::function a deep dive behind the curtain co_await question_ready()
Thank you for your attention!
Now we are free to discuss any questions.
Bonus Quiz
What is the size? (1) int demo(double x) {
return static_cast<int>(x);
}
int main() {
std::cout << sizeof(demo);
}
Size of the function is undefined.
All compilers reject that.
gcc has option to allow this. would print 1.
What is the size? (2) int demo(double x) {
return static_cast<int>(x);
}
int main() {
auto ptr = &demo;
std::cout << sizeof(ptr);
}
That’s just a normal pointer.
8 bytes on 64 bits.
4 bytes on 32 bits.
What is the size? (3) struct Demo {
int memberFunc(double x) {
return static_cast<int>(x);
}
};
int main() {
auto ptr = &Demo::memberFunc;
std::cout << sizeof(ptr);
Demo demo{};
std::cout << (demo.*ptr)(3.14);
}
Reduce Size for members struct Demo {
int memberFunc(double x) {
return static_cast<int>(x);
}
};
using DemoFunc = int(Demo*, double);
constexpr DemoFunc* ptr = [](Demo* demo, double x) {
return demo->memberFunc(x);
};
int main() {
std::cout << sizeof(ptr);
Demo demo{};
std::cout << ptr(&demo, 3.14);
}
To call a member function:
* object pointer for this
* arguments
A Lambda without a capture is just a function pointer.